﻿using Common.FrontEnd;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Util.Props;

namespace FrontEnd.Feature
{
    /**
    /// Subtracts the mean of all the input so far from the Data objects.
    /// 
    /// Unlike the {@link BatchCMN}, it does not read in the entire stream of Data
    /// objects before it calculates the mean. It estimates the mean from already
    /// seen data and subtracts the mean from the Data objects on the fly. Therefore,
    /// there is no delay introduced by LiveCMN in general. The only real issue is an
    /// initial CMN estimation, for that some amount of frames are read initially
    /// and cmn estimation is calculated from them.
    /// <p/>
    /// The properties that affect this processor are defined by the fields
    /// {@link #PROP_INITIAL_CMN_WINDOW}, {@link #PROP_CMN_WINDOW}, and
    /// {@link #PROP_CMN_SHIFT_WINDOW}. Please follow the link
    /// "Constant Field Values" below to see the actual name of the Sphinx
    /// properties.
    /// <p/>
    /// <p>
    /// The mean of all the input cepstrum so far is not reestimated for each
    /// cepstrum. This mean is recalculated after every
    /// {@link #PROP_CMN_SHIFT_WINDOW} cepstra. This mean is estimated by dividing
    /// the sum of all input cepstrum so far. After obtaining the mean, the sum is
    /// exponentially decayed by multiplying it by the ratio:
    /// 
    /// <pre>
    /// cmnWindow/(cmnWindow + number of frames since the last recalculation)
    /// </pre>
    /// <p/>
    /// 
    /// @see BatchCMN
     */
    public class LiveCMN:BaseDataProcessor
    {
        private String formatter;
        //= new DecimalFormat("0.00;-0.00", new DecimalFormatSymbols(Locale.US));;

        /** The property for the live CMN initial window size. */
        [S4Integer(defaultValue = 200)]
        public static String PROP_INITIAL_CMN_WINDOW = "initialCmnWindow";
        private int initialCmnWindow;
    
        /** The property for the live CMN window size. */
        [S4Integer(defaultValue = 100)]
        public static String PROP_CMN_WINDOW = "cmnWindow";
        private int cmnWindow;

        /**
        /// The property for the CMN shifting window. The shifting window specifies
        /// how many cepstrum after which we re-calculate the cepstral mean.
         */
        [@S4Integer(defaultValue = 160)]
        public static String PROP_CMN_SHIFT_WINDOW = "shiftWindow";
        private int cmnShiftWindow; // # of Cepstrum to recalculate mean

        private double[] currentMean; // array of current means
        private double[] sum; // array of current sums
        private int numberFrame; // total number of input Cepstrum

        List<IData> initialList;

        public LiveCMN(double initialMean, int cmnWindow, int cmnShiftWindow, int initialCmnWindow) 
        {
            this.cmnWindow = cmnWindow;
            this.cmnShiftWindow = cmnShiftWindow;
            this.initialCmnWindow = initialCmnWindow;
        }

        public LiveCMN() {

        }

        override
        public void newProperties(PropertySheet ps)
        {
            base.newProperties(ps);
            cmnWindow = ps.getInt(PROP_CMN_WINDOW);
            cmnShiftWindow = ps.getInt(PROP_CMN_SHIFT_WINDOW);
            initialCmnWindow = ps.getInt(PROP_INITIAL_CMN_WINDOW);
        }

        /** Initializes this LiveCMN. */
        public void initialize() 
        {
            base.initialize();
        }

        /**
        /// Initializes the currentMean and sum arrays with the given cepstrum
        /// length.
        /// 
        /// @param cepstrumLength
        ///            the length of the cepstrum
         */
        private void initMeansSums() {
            int size = -1;
        
            foreach (IData data in initialList) 
            {
                if (!(data is DoubleData))
                    continue;
        
                double[] cepstrum = ((DoubleData) data).Values;
            
                // Initialize arrays if needed
                if (size < 0) {
                    size = cepstrum.Length;
                    sum = new double[size];
                    numberFrame = 0;
                }
                // Process
                for (int j = 0; j < size; j++) {
                    sum[j] += cepstrum[j];
                }
                numberFrame++;
            }

            // If we didn't meet any data, do nothing
            if (size < 0)
                return;

            currentMean = new double[size];
            for (int j = 0; j < size; j++) {
                currentMean[j] = sum[j] / numberFrame;
            }
        }

        /**
        /// Returns the next Data object, which is a normalized Data produced by this
        /// class. Signals are returned unmodified.
        /// 
        /// @return the next available Data object, returns null if no Data object is
        ///         available
        /// @throws DataProcessingException
        ///             if there is a data processing error
         */
        public IData getData()
        {

            IData input, output;
            getTimer().start();

            if (initialList == null) 
            {
                initialList = new List<IData>();
                // Collect initial data for estimation
                while (initialList.Count < initialCmnWindow) 
                {
                    input = getPredecessor().getData();
                    initialList.Add(input);
                    if (input is SpeechEndSignal
                            || input is DataEndSignal)
                        break;
                }
                initMeansSums();
                output = initialList[0];
                initialList.RemoveAt(0);
            } 
            else if (initialList.Count!=0) {
                // Return the previously collected data
                output = initialList[0];
                initialList.RemoveAt(0);
            } else {
                // Process normal frame
                output = getPredecessor().getData();
            }

            normalize(output);
            getTimer().stop();
            return output;
        }

        /**
        /// Normalizes the given Data with using the currentMean array. Updates the
        /// sum array with the given Data.
        /// 
        /// @param cepstrumObject
        ///            the Data object to normalize
         */
        private void normalize(IData data) 
        {

            if (!(data is DoubleData))
                return;

            double[] cepstrum = ((DoubleData) data).Values;

            if (cepstrum.Length != sum.Length) {
                throw new Exception("Data length (" + cepstrum.Length
                        + ") not equal sum array length (" + sum.Length + ')');
            }

            for (int j = 0; j < cepstrum.Length; j++) {
                sum[j] += cepstrum[j];
                cepstrum[j] -= currentMean[j];
            }

            numberFrame++;

            if (numberFrame > cmnShiftWindow) {
            
                StringBuilder cmn = new StringBuilder();
                // calculate the mean first
                for (int i = 0; i < currentMean.Length; i++) 
                {
                    cmn.Append (currentMean[i].ToString());
                    cmn.Append(' ');
                }
                Trace.WriteLine(cmn.ToString());
            
                updateMeanSumBuffers();
            }
        }

        /**
        /// Updates the currentMean buffer with the values in the sum buffer. Then
        /// decay the sum buffer exponentially, i.e., divide the sum with
        /// numberFrames.
         */
        private void updateMeanSumBuffers() {

            // update the currentMean buffer with the sum buffer
            double sf = 1.0 / numberFrame;

            Array.Copy(sum, 0, currentMean, 0, sum.Length);

            multiplyArray(currentMean, sf);

            // decay the sum buffer exponentially
            if (numberFrame >= cmnShiftWindow) {
                multiplyArray(sum, (sf* cmnWindow));
                numberFrame = cmnWindow;
            }
        }

        /**
        /// Multiplies each element of the given array by the multiplier.
        /// 
        /// @param array
        ///            the array to multiply
        /// @param multiplier
        ///            the amount to multiply by
         */
        private static void multiplyArray(double[] array, double multiplier) {
            for (int i = 0; i < array.Length; i++) {
                array[i] *= multiplier;
            }
        }

    }
}
